/**
 * Copyright 2006 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy 
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0 
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations 
 * under the License.
 */

package com.google.appsforyourdomain.provisioning;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.jdom.Document;

import com.google.appsforyourdomain.provisioning.Account.AccountStatus;
import com.google.appsforyourdomain.provisioning.MailingList.ListOperation;

/**
 * The ProvisioningClient wraps the functions provided by the
 * Google Apps for Your Domain Provisioning API v1.0.
 * This API enables users to create, retrieve, update and delete hosted 
 * accounts. Users can also use the API to create email accounts, user 
 * aliases and mailing lists.
 * <p>
 * EXAMPLE USAGE:
 * <pre>
 * public static void main(String[] args) {
 *   try {
 *     String domain = "example.com";
 *     String adminuser = "admin@example.com";
 *     String password = "password";
 *     String newuser = "newuser";
 *     String newalias = "newalias";
 *     String newmailinglist = "newmailinglist";
 *
 *     ProvisioningClient prov = new ProvisioningClient();
 *     
 *     // Get token
 *     String token = prov.getAuthenticationToken(adminuser, password);
 *     
 *     // Create new account
 *     prov.createAccountWithEmail("Firstname", "LastName", newuser,
 *         "userpassword", domain, token);
 *     
 *     // Lock an account
 *     prov.updateAccountStatus(newuser, 
 *       Account.AccountStatus.locked, domain, token );
 *       
 *     // Retrieve an account
 *     Account account = prov.retrieveAccount(newuser, domain, token);
 *     System.out.println("First Name: " + account.getFirstName());
 *     System.out.println("Last Name: " + account.getLastName());
 *     System.out.println("Username: " + account.getUserName());
 *     System.out.println("AliasName: " + account.getAliases());
 *     System.out.println("EmailLists: " + account.getEmailLists());
 *     System.out.println("AccountStatus: " + account.getAccountStatus());
 *     
 *     // Create new alias
 *     prov.createAlias(newuser, newalias, domain, token);
 *     
 *     // Retrieve an alias
 *     Alias alias = prov.retrieveAlias(newalias, domain, token);
 *     System.out.println("Alias Name: " + alias.getAliasName());
 *     System.out.println("Email Address: ");
 *     System.out.println(alias.getEmailAddress());
 *     
 *     // Create a new mailing list
 *     prov.createMailingList(newmailinglist, domain, token);
 *     
 *     // Add a user to a mailing list
 *     prov.updateMailingList(newmailinglist, newuser, 
 *       MailingList.ListOperation.add, domain, token);
 *     
 *     // Retrieve a mailing list
 *     MailingList ml = prov.retrieveMailingList(newmailinglist, domain, token);
 *     System.out.println("Mailing List Name: " + ml.getMailingListName());
 *     System.out.println("Email addresses: ");
 *     System.out.println(ml.getEmailAddresses());
 *      
 *     // Delete operations
 *     prov.deleteAlias(newalias, domain, token);
 *     prov.deleteMailingList(newmailinglist, domain, token);
 *     prov.deleteAccount(newuser, domain, token);
 *   } catch (AppsForYourDomainException e) {
 *     System.out.println("Exception: " + e.getReason());
 *     System.out.println("Exception Details: " + e.getExtendedMessage());
 *   }
 * }
 * </pre>
 */
public class ProvisioningClient {

  // BASE URL to access Google Apps for Your Domain API
  protected static final String BASE_URL = 
      "https://www.google.com/a/services/v1.0/";
  
  // Client Authentication URL
  protected static final String CLIENT_LOGIN_URL = 
      "https://www.google.com/accounts/ClientLogin";
  
  // Various REST response XPATH
  protected static final String STATUS_PATH = "/hs:rest/hs:status";
  protected static final String REASON_PATH = "/hs:rest/hs:reason";
  protected static final String MESSAGE_PATH = "/hs:rest/hs:extendedMessage";
  protected static final String EMAIL_PATH = 
      "/hs:rest/hs:RetrievalSection/hs:emailAddresses/hs:emailAddress";
  
  /**
   * Obtains an authorization token for the specified domain
   * administrator.
   * 
   * @param adminEmail Admin email address
   * @param adminPassword Admin password
   * @return Authentication token
   * @throws AppsForYourDomainException
   */
  public String getAuthenticationToken(String adminEmail, String adminPassword) 
      throws AppsForYourDomainException {
    return getAuthenticationToken(adminEmail, adminPassword, "", "");
  } 
  
  /**
   * Obtains an authorization token for the specified domain
   * administrator.  The adminEmail and adminPassword
   * parameteters are always required.  captchaToken and
   * captchaResponse should be the empty string unless
   * a CAPTCHA response is required.
   * 
   * @param adminEmail Admin email address
   * @param adminPassword Admin password
   * @param captchaToken CAPTCHA token
   * @param captchaResponse CAPTCHA response
   * @return Authentication token
   * @throws AppsForYourDomainException
   */
  public String getAuthenticationToken(String adminEmail, String adminPassword,
      String captchaToken, String captchaResponse) 
      throws AppsForYourDomainException {
    try {
      String authToken = "";
      // Construct content
      String postContent = generateAuthenticationRequest(
          adminEmail, adminPassword, captchaToken, captchaResponse);
      // Send content
      String url = CLIENT_LOGIN_URL;
      HttpClient client = new HttpClient();
      PostMethod method = new PostMethod( url );
      method.setRequestHeader("Content-Type",
          "application/x-www-form-urlencoded");
      StringRequestEntity sre = new StringRequestEntity(postContent);
      method.setRequestEntity(sre);

      int statusCode = client.executeMethod(method);
      if (statusCode != 200) {
        throw new AuthenticationException(statusCode + ": " + 
            method.getResponseBodyAsString());
      } else {
        String response = method.getResponseBodyAsString();
        if (response.startsWith("SID=")) {
          authToken = response.substring(4, response.indexOf('\n'));
        }
      }
      return authToken;
    } catch (IOException e) {
      // error in URL Connection
      throw new ConnectionException(e.getMessage());
    }
  } 
  
  /**
   * Generates the authentication request. captchaToken and
   * captchaResponse should be the empty string unless
   * a CAPTCHA response is required.
   * 
   * @param adminEmail Admin email address
   * @param adminPassword Admin password
   * @param captchaToken CAPTCHA token
   * @param captchaResponse CAPTCHA response
   * @return Authentication request string
   */
  private String generateAuthenticationRequest(
      String adminEmail, String adminPassword, String captchaToken, 
      String captchaResponse) {
    try {
      StringBuffer buffer = new StringBuffer("accountType=HOSTED&Email=");
      buffer.append(URLEncoder.encode(adminEmail, "UTF-8"));
      buffer.append("&Passwd=");
      buffer.append(URLEncoder.encode(adminPassword, "UTF-8"));
      if (captchaToken != "" && captchaToken != null) {
        buffer.append("&logintoken=");
        buffer.append(URLEncoder.encode(captchaToken, "UTF-8"));
      }
      if (captchaResponse != "" && captchaResponse != null) {
        buffer.append("&logincaptcha=");
        buffer.append(URLEncoder.encode(captchaResponse, "UTF-8"));
      }
      return buffer.toString();
    } catch (UnsupportedEncodingException e) {
      return "";
    }
  }
  
  /**
   * Creates a new user account with the specified first name,
   * last name, username and password. The user email will be
   * automatically created with default quota and with the address 
   * username@domain. An authorization token 
   * (obtained with getAuthenticationToken()) and the domain must 
   * also be provided.
   * 
   * @param firstName User's firstname
   * @param lastName User's lastname
   * @param userName User's username
   * @param password User's password
   * @param domainName Domain name of the account
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void createAccountWithEmail(String firstName, String lastName,
      String userName, String password, String domainName, String authToken)
      throws AppsForYourDomainException {
    createAccountWithEmail(firstName, lastName, userName, password, 0, 
        domainName, authToken);
  }
  /**
   * Creates a new user account with the specified first name,
   * last name, username and password. The user email will be
   * automatically created with a custom quota and with the address 
   * username@domain. An authorization token 
   * (obtained with getAuthenticationToken()) and the domain must 
   * also be provided.
   * 
   * @param firstName User's firstname
   * @param lastName User's lastname
   * @param userName User's username
   * @param password User's password
   * @param quota Email quota in megabytes (valid only for domains with 
   *              custom quota). Set quota to 0 for default quota.
   * @param domainName Domain name of the account
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void createAccountWithEmail(String firstName, String lastName, String userName, 
      String password, int quota, String domainName, String authToken)
      throws AppsForYourDomainException {
    String postContent = Account.generateCreateAccountRequest(firstName, 
        lastName, password, userName, quota, domainName, authToken);
    String url = BASE_URL + "Create/Account/Email";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Updates the user account specified by userName.  This
   * function can only modify the first name, last name or
   * password properties of the account. To leave fields 
   * unchanged, specify their values as the empty string.
   *
   * An authorization token (obtained with getAuthenticationToken()) 
   * and the domain must also be provided.
   * 
   * @param firstName Account firstname
   * @param lastName Account lastname
   * @param userName Account Username
   * @param password Account password
   * @param domainName Account Domain name
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void updateAccount(String firstName, String lastName,
      String userName, String password, String domainName, String authToken) 
      throws AppsForYourDomainException {
    String postContent = Account.generateUpdateAccountRequest(firstName, 
        lastName, password, userName, domainName, authToken);
    String url = BASE_URL + "Update/Account";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Retrieves information about the specified username.
   * An authorization token (obtained with getAuthenticationToken())
   * and the domain must also be provided.
   * 
   * @param userName Account username
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @return The Account object
   * @throws AppsForYourDomainException
   */
  public Account retrieveAccount(String userName, String domainName, 
      String authToken) throws AppsForYourDomainException {   
    String postContent = Account.generateAccountRequest(userName,
        domainName, authToken);
    String url = BASE_URL + "Retrieve/Account";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("success")) {
      Account account = Account.createAccountFromXmlDocument(doc);
      return account;
    } else {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Deletes the specified account. An authorization token
   * (obtained with getAuthenticationToken()) and the domain must
   * also be provided.
   * 
   * @param userName Account username
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void deleteAccount(String userName, String domainName, 
      String authToken) throws AppsForYourDomainException {
    String postContent = Account.generateAccountRequest(userName,
        domainName, authToken);
    String url = BASE_URL + "Delete/Account";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Updates the specified user's account status.  If status
   * is "locked", the user's email account is disabled; if
   * status is "unlocked", the user's email account is enabled.
   *
   * An authorization token and domain must also be provided.
   * 
   * @param userName Account username
   * @param accountStatus Account status (locked or unlocked)
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void updateAccountStatus(String userName, AccountStatus accountStatus,
      String domainName, String authToken) throws AppsForYourDomainException {
    String postContent = Account.generateUpdateAccountStatusRequest(userName, 
        accountStatus, domainName, authToken);
    String url = BASE_URL + "Update/Account/Status";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Creates a new alias named alias_name for the specified user.
   * An authorization token (obtained with getAuthenticationToken()) 
   * and the domain must also be provided.
   * 
   * @param userName Username to be aliased
   * @param aliasName Email alias name
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void createAlias(String userName, String aliasName, String domainName,
      String authToken) throws AppsForYourDomainException {
    String postContent = Alias.generateCreateAliasRequest(userName, aliasName,
        domainName, authToken);
    String url = BASE_URL + "Create/Alias";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
 
  /**
   * Retrieves information about the specified alias.
   * An authorization token (obtained with getAuthenticationToken())
   * and the domain must also be provided.
   * 
   * @param aliasName Alias name
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @return Alias object
   * @throws AppsForYourDomainException
   */
  public Alias retrieveAlias(String aliasName, String domainName, 
      String authToken) throws AppsForYourDomainException {
    String postContent = Alias.generateAliasRequest(aliasName,
        domainName, authToken);
    String url = BASE_URL + "Retrieve/Alias";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("success")) {
      Alias alias = new Alias(aliasName, 
          AppsUtil.parseXml(doc, EMAIL_PATH));
      return alias;
    } else {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Deletes the specified alias. An authorization token
   * (obtained with getAuthenticationToken()) and the domain must
   * also be provided.
   * 
   * @param aliasName Alias name
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void deleteAlias(String aliasName, String domainName, 
      String authToken) throws AppsForYourDomainException {
    String postContent = Alias.generateAliasRequest(aliasName, domainName, 
        authToken);
    String url = BASE_URL + "Delete/Alias";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Creates a new mailing list named mailingListName. An
   * authorization token (obtained with getAuthenticationToken())
   * and the domain must also be provided.
   * 
   * @param mailingListName Name of mailing list
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void createMailingList(String mailingListName, String domainName, 
      String authToken) throws AppsForYourDomainException {
    String postContent = MailingList.generateCreateMailingListRequest(
        mailingListName, domainName, authToken);
    String url = BASE_URL + "Create/MailingList";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Updates the mailing list specified by mailingListName
   * as follows: If operation is "add", userName is added to
   * the mailing list.  If operation is "remove", userName is
   * removed from the mailing list.
   *
   * An authorization token (obtained with getAuthenticationToken())
   * and the domain must also be provided.
   * 
   * @param mailingListName Name of mailing list
   * @param userName Username to be added or removed
   * @param operation add or remove
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void updateMailingList(String mailingListName, String userName,
      ListOperation operation, String domainName, String authToken) 
      throws AppsForYourDomainException {
    String postContent = MailingList.generateUpdateMailingListRequest(
        mailingListName, userName, operation, domainName, authToken);
    String url = BASE_URL + "Update/MailingList";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Retrieves information about the specified mailing list.
   * An authorization token (obtained with getAuthenticationToken())
   * and the domain must also be provided.
   * 
   * @param mailingListName Name of mailing list
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @return MailingList object
   * @throws AppsForYourDomainException
   */
  public MailingList retrieveMailingList(String mailingListName, 
      String domainName, String authToken) throws AppsForYourDomainException {  
    String postContent = MailingList.generateMailingListRequest(
        mailingListName, domainName, authToken);
    String url = BASE_URL + "Retrieve/MailingList";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("success")) {
      MailingList mailingList = new MailingList(mailingListName, 
          AppsUtil.parseXmlMultipleNodes(doc, EMAIL_PATH));
      return mailingList;
    } else {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
  
  /**
   * Deletes the specified mailing list. An authorization token
   * (obtained with getAuthenticationToken()) and the domain must also
   * be provided.
   * 
   * @param mailingListName Name of mailing list
   * @param domainName Account domain name
   * @param authToken Authentication token
   * @throws AppsForYourDomainException
   */
  public void deleteMailingList(String mailingListName, String domainName, 
      String authToken) throws AppsForYourDomainException {  
    String postContent = MailingList.generateMailingListRequest(
        mailingListName, domainName, authToken);
    String url = BASE_URL + "Delete/MailingList";
    Document doc = AppsUtil.postHttpRequest(url, postContent);
    String status = AppsUtil.parseXml(doc, STATUS_PATH);
    if (status.toLowerCase().contains("failure")) {
      throw new AppsForYourDomainException(AppsUtil.parseXml(doc, REASON_PATH), 
          AppsUtil.parseXml(doc, MESSAGE_PATH));
    }
  }
}
